#!/usr/bin/env ruby

# -------------------------------------------------------------------------- #
# Copyright 2002-2016, OpenNebula Project, OpenNebula Systems                #
#                                                                            #
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
# not use this file except in compliance with the License. You may obtain    #
# a copy of the License at                                                   #
#                                                                            #
# http://www.apache.org/licenses/LICENSE-2.0                                 #
#                                                                            #
# Unless required by applicable law or agreed to in writing, software        #
# distributed under the License is distributed on an "AS IS" BASIS,          #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
# See the License for the specific language governing permissions and        #
# limitations under the License.                                             #
#--------------------------------------------------------------------------- #

ONE_LOCATION=ENV["ONE_LOCATION"]

if !ONE_LOCATION
    RUBY_LIB_LOCATION="/usr/lib/one/ruby"
else
    RUBY_LIB_LOCATION=ONE_LOCATION+"/lib/ruby"
end

$: << RUBY_LIB_LOCATION
$: << RUBY_LIB_LOCATION+'/cli'

require 'command_parser'
require 'opennebula/oneflow_client'

require 'cli_helper'
require 'one_helper/onevm_helper'

require 'json'

USER_AGENT = "CLI"

# Base Path representing the resource to be used in the requests
RESOURCE_PATH = "/service"

#
# Table
#

SERVICE_TABLE = CLIHelper::ShowTable.new(nil, self) do
    column :ID, "ID", :size=>10 do |d|
        d["ID"]
    end

    column :USER, "Username", :left, :size=>15 do |d|
        d["UNAME"]
    end

    column :GROUP, "Group", :left, :size=>15 do |d|
        d["GNAME"]
    end

    column :NAME, "Name", :size=>25, :left=>true do |d|
        d["NAME"]
    end

    column :STATE, "State", :size=>11, :left=>true do |d|
        Service.state_str(d["TEMPLATE"]["BODY"]['state'])
    end

    default :ID, :USER, :GROUP, :NAME, :STATE
end

NODE_TABLE = CLIHelper::ShowTable.new(nil, self) do
    column :VM_ID, "ONE identifier for Virtual Machine", :size=>6 do |d|
        st = ""
        if d['scale_up']
            st << "\u2191 "
        elsif d['disposed']
            st << "\u2193 "
        end

        if d['vm_info'].nil?
            st << d['deploy_id'].to_s
        else
            st << d['vm_info']['VM']["ID"]
        end

        st
    end

    column :NAME, "Name of the Virtual Machine", :left,
            :size=>23 do |d|
        if !d['vm_info'].nil?
            if d['vm_info']['VM']["RESCHED"] == "1"
                "*#{d["NAME"]}"
            else
                d['vm_info']['VM']["NAME"]
            end
        else
            ""
        end
    end

    column :USER, "Username of the Virtual Machine owner", :left,
            :size=>8 do |d|
        if !d['vm_info'].nil?
            d['vm_info']['VM']["UNAME"]
        else
            ""
        end
    end

    column :GROUP, "Group of the Virtual Machine", :left,
            :size=>8 do |d|
        if !d['vm_info'].nil?
            d['vm_info']['VM']["GNAME"]
        else
            ""
        end
    end

    column :STAT, "Actual status", :size=>4 do |d,e|
        if !d['vm_info'].nil?
            OneVMHelper.state_to_str(d['vm_info']['VM']["STATE"], d['vm_info']['VM']["LCM_STATE"])
        else
            ""
        end
    end

    column :UCPU, "CPU percentage used by the VM", :size=>4 do |d|
        if !d['vm_info'].nil?
            d['vm_info']['VM']["CPU"]
        else
            ""
        end
    end

    column :UMEM, "Memory used by the VM", :size=>7 do |d|
        if !d['vm_info'].nil?
            OpenNebulaHelper.unit_to_str(d['vm_info']['VM']["MEMORY"].to_i, {})
        else
            ""
        end
    end

    column :HOST, "Host where the VM is running", :left, :size=>20 do |d|
        if !d['vm_info'].nil?
            if d['vm_info']['VM']['HISTORY_RECORDS'] && d['vm_info']['VM']['HISTORY_RECORDS']['HISTORY']
                state_str = VirtualMachine::VM_STATE[d['vm_info']['VM']['STATE'].to_i]
                if %w{ACTIVE SUSPENDED}.include? state_str
                    history = if d['vm_info']['VM']['HISTORY_RECORDS']['HISTORY'].instance_of?(Array)
                        d['vm_info']['VM']['HISTORY_RECORDS']['HISTORY'].last
                    else
                        d['vm_info']['VM']['HISTORY_RECORDS']['HISTORY']
                    end

                    history['HOSTNAME']
                end
            end
        else
            ""
        end
    end

    column :TIME, "Time since the VM was submitted", :size=>10 do |d|
        if !d['vm_info'].nil?
            stime = d['vm_info']['VM']["STIME"].to_i
            etime = d['vm_info']['VM']["ETIME"]=="0" ? Time.now.to_i : d['vm_info']['VM']["ETIME"].to_i
            dtime = etime-stime
            OpenNebulaHelper.period_to_str(dtime, false)
        else
            ""
        end
    end

    default :VM_ID, :NAME, :STAT, :UCPU, :UMEM, :HOST, :TIME
end

# List the services. This method is used in top and list commands
# @param [Service::Client] client
# @param [Array] args
# @param [Hash] options
# @return [[Integer, String], Integer] Returns the exit_code and optionally
#   a String to be printed
def list_services(client, args, options)
    response = client.get(RESOURCE_PATH)

    if CloudClient::is_error?(response)
        [response.code.to_i, response.to_s]
    else
        #[0,response.body]
        if options[:json]
            [0,response.body]
        else
            array_list = JSON.parse(response.body)
            SERVICE_TABLE.show(array_list['DOCUMENT_POOL']['DOCUMENT'])
            0
        end
    end
end

# Show the service information. This method is used in top and show commands
# @param [Service::Client] client
# @param [Array] args
# @param [Hash] options
# @return [[Integer, String], Integer] Returns the exit_code and optionally
#   a String to be printed
def show_service(client, args, options)
    response = client.get("#{RESOURCE_PATH}/#{args[0]}")

    if CloudClient::is_error?(response)
        [response.code.to_i, response.to_s]
    else
        #[0,response.body]
        if options[:json]
            [0,response.body]
        else
            str="%-20s: %-20s"
            str_h1="%-80s"

            document_hash = JSON.parse(response.body)
            template = document_hash['DOCUMENT']['TEMPLATE']['BODY']

            CLIHelper.print_header(str_h1 % "SERVICE #{document_hash['DOCUMENT']['ID']} INFORMATION")

            puts str % ["ID",   document_hash['DOCUMENT']['ID']]
            puts str % ["NAME", document_hash['DOCUMENT']['NAME']]
            puts str % ["USER", document_hash['DOCUMENT']['UNAME']]
            puts str % ["GROUP",document_hash['DOCUMENT']['GNAME']]

            puts str % ["STRATEGY", template['deployment']]
            puts str % ["SERVICE STATE", Service.state_str(template['state'])]
            puts str % ["SHUTDOWN", template['shutdown_action']] if template['shutdown_action']

            puts

            CLIHelper.print_header(str_h1 % "PERMISSIONS",false)

            ["OWNER", "GROUP", "OTHER"].each { |e|
                mask = "---"
                mask[0] = "u" if document_hash['DOCUMENT']['PERMISSIONS']["#{e}_U"] == "1"
                mask[1] = "m" if document_hash['DOCUMENT']['PERMISSIONS']["#{e}_M"] == "1"
                mask[2] = "a" if document_hash['DOCUMENT']['PERMISSIONS']["#{e}_A"] == "1"

                puts str % [e,  mask]
            }

            puts

            template['roles'].each {|role|
                CLIHelper.print_header("ROLE #{role['name']}", false)

                puts str % ["ROLE STATE", Role.state_str(role['state'])]
                puts str % ["PARENTS", role['parents'].join(', ')] if role['parents']
                puts str % ["VM TEMPLATE", role['vm_template']]
                puts str % ["CARDINALITY", role['cardinality']]
                puts str % ["MIN VMS", role['min_vms']] if role['min_vms']
                puts str % ["MAX VMS", role['max_vms']] if role['max_vms']
                puts str % ["COOLDOWN", "#{role['cooldown']}s"] if role['cooldown']
                puts str % ["SHUTDOWN", role['shutdown_action']] if role['shutdown_action']

                puts "NODES INFORMATION"
                NODE_TABLE.show(role['nodes'])

                if !role['elasticity_policies'].nil? && role['elasticity_policies'].size > 0 || !role['scheduled_policies'].nil? && role['scheduled_policies'].size > 0
                    puts
                    puts "ELASTICITY RULES"

                    if role['elasticity_policies'] && role['elasticity_policies'].size > 0
                        puts
#                        puts "ELASTICITY POLICIES"
                        CLIHelper::ShowTable.new(nil, self) do
                            column :ADJUST, "", :left, :size=>12 do |d|
                                adjust_str(d)
                            end

                            column :EXPRESSION, "", :left, :size=>48 do |d|
                                if !d['expression_evaluated'].nil?
                                    d['expression_evaluated']
                                else
                                    d['expression']
                                end
                            end

                            column :'EVALS', "", :right, :size=>5 do |d|
                                if d['period_number']
                                    "#{d['true_evals'].to_i}/#{d['period_number']}"
                                else
                                    "-"
                                end
                            end

                            column :PERIOD, "", :size=>6 do |d|
                                d['period'] ?  "#{d['period']}s" : '-'
                            end

                            column :COOL, "", :size=>5 do |d|
                                d['cooldown'] ? "#{d['cooldown']}s" : '-'
                            end

                            default :ADJUST, :EXPRESSION, :EVALS, :PERIOD, :COOL
                        end.show([role['elasticity_policies']].flatten, {})
                    end

                    if role['scheduled_policies'] && role['scheduled_policies'].size > 0
                        puts
#                        puts "SCHEDULED POLICIES"
                        CLIHelper::ShowTable.new(nil, self) do
                            column :ADJUST, "", :left, :size=>12 do |d|
                                adjust_str(d)
                            end

                            column :TIME, "", :left, :size=>67 do |d|
                                if d['start_time']
                                    Time.parse(d['start_time']).to_s
                                else
                                    d['recurrence']
                                end
                            end

                            default :ADJUST, :TIME
                        end.show([role['scheduled_policies']].flatten, {})
                    end
                end

                puts
            }

            puts

            CLIHelper.print_header(str_h1 % "LOG MESSAGES",false)

            if template['log']
                template['log'].each { |log|
                    t = Time.at(log['timestamp']).strftime("%m/%d/%y %H:%M")
                    puts "#{t} [#{log['severity']}] #{log['message']}"
                }
            end

            0
        end
    end
end

def adjust_str(policy)
    sign = policy['adjust'].to_i >= 0 ? "+" : "-"
    adjust = policy['adjust'].to_i.abs

    case policy['type']
    when 'CARDINALITY'
        "= #{adjust}"
    when 'PERCENTAGE_CHANGE'
        st = "#{sign} #{adjust} %"
        if policy['min_adjust_step']
            st << " (#{policy['min_adjust_step']})"
        end

        st
    else
        "#{sign} #{adjust}"
    end
end

#
# Commands
#

cmd=CommandParser::CmdParser.new(ARGV) do
    usage "`oneflow` <command> [<args>] [<options>]"
    version OpenNebulaHelper::ONE_VERSION

    set :option, Service::DEFAULT_OPTIONS
    set :option, CommandParser::VERSION
    set :option, CommandParser::HELP

    #
    # Formatters for arguments
    #
    set :format, :groupid, OpenNebulaHelper.rname_to_id_desc("GROUP") do |arg|
        OpenNebulaHelper.rname_to_id(arg, "GROUP")
    end

    set :format, :userid, OpenNebulaHelper.rname_to_id_desc("USER") do |arg|
        OpenNebulaHelper.rname_to_id(arg, "USER")
    end

    set :format, :service_id, Service.rname_to_id_desc("SERVICE") do |arg|
        Service.rname_to_id(arg, "SERVICE")
    end

    set :format, :service_id_list, Service.list_to_id_desc("SERVICE") do |arg|
        Service.list_to_id(arg, "SERVICE")
    end

    set :format, :vm_action, "Actions supported: #{Role::SCHEDULE_ACTIONS.join(', ')}" do |arg|
        if Role::SCHEDULE_ACTIONS.include?(arg)
            [0, arg]
        else
            [-1, "Action #{arg} is not supported. Actions supported: #{Role::SCHEDULE_ACTIONS.join(', ')}"]
        end
    end

    #
    # List
    #

    list_desc = <<-EOT.unindent
        List the available services
    EOT

    command :list, list_desc, :options => Service::JSON_FORMAT do
        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        list_services(client, args, options)
    end

    #
    # Show
    #

    show_desc = <<-EOT.unindent
        Show detailed information of a given service
    EOT

    command :show, show_desc, :service_id, :options => Service::JSON_FORMAT do
        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        show_service(client, args, options)
    end

    #
    # Top
    #

    top_desc = <<-EOT.unindent
        Top the services or the extended information of the target service if a
        id is specified
    EOT

    command :top, top_desc, [:service_id, nil],
            :options => [Service::JSON_FORMAT, Service::TOP, CLIHelper::DELAY] do
        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        delay=options[:delay] ? options[:delay] : 3

        begin
            while true
                CLIHelper.scr_cls
                CLIHelper.scr_move(0,0)

                if args[0]
                    rc, message = show_service(client, args, options)

                    if rc != 0
                        raise message
                    end
                else
                    rc, message = list_services(client, args, options)

                    if rc != 0
                        raise message
                    end
                end

                sleep delay
            end
        rescue Exception => e
            puts e.message
            -1
        end
    end

    #
    # Delete
    #

    delete_desc = <<-EOT.unindent
        Delete a given service
    EOT

    command :delete, delete_desc, [:range, :service_id_list] do
        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        Service.perform_actions(args[0]) { |service_id|
            client.delete("#{RESOURCE_PATH}/#{service_id}")
        }
    end

    #
    # Shutdown
    #

    shutdown_desc = <<-EOT.unindent
        Shutdown a service.
            From RUNNING or WARNING shuts down the Service
    EOT

    command :shutdown, shutdown_desc, [:range, :service_id_list] do
        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        Service.perform_actions(args[0]) { |service_id|
            json_action = Service.build_json_action('shutdown')

            client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action)
        }
    end

    #
    # Recover
    #

    recover_desc = <<-EOT.unindent
        Recover a failed service, cleaning the failed VMs.
            From FAILED_DEPLOYING continues deploying the Service
            From FAILED_SCALING continues scaling the Service
            From FAILED_UNDEPLOYING continues shutting down the Service
            From COOLDOWN the Service is set to running ignoring the cooldown duration
            From WARNING failed VMs are deleted, and new VMs are instantiated
    EOT

    command :recover, recover_desc, [:range, :service_id_list] do
        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        Service.perform_actions(args[0]) { |service_id|
            json_action = Service.build_json_action('recover')

            client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action)
        }
    end

    #
    # Scale
    #

    scale_desc = <<-EOT.unindent
        Scale a role to the given cardinality
    EOT

    command :'scale', scale_desc, :service_id, :role_name,
        :cardinality, :options => [Service::FORCE] do
        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        if !(args[2] =~ /^\d+$/)
            puts "Cardinality must be an integer number"
            exit -1
        end

        exit_code = 0

        json =  "{ \"cardinality\" : #{args[2]},\n"<<
                "  \"force\" : #{options[:force] == true} }"

        response = client.put("#{RESOURCE_PATH}/#{args[0]}/role/#{args[1]}", json)

        if CloudClient::is_error?(response)
            puts response.to_s
            exit_code = response.code.to_i
        end

        exit_code
    end

    chgrp_desc = <<-EOT.unindent
        Changes the service group
    EOT

    command :chgrp, chgrp_desc, [:range, :service_id_list], :groupid do
        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        Service.perform_actions(args[0]) { |service_id|
            params = Hash.new
            params['group_id'] = args[1].to_i

            json_action = Service.build_json_action('chgrp', params)

            client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action)
        }
    end

    chown_desc = <<-EOT.unindent
        Changes the service owner and group
    EOT

    command :chown, chown_desc, [:range, :service_id_list], :userid, [:groupid, nil] do
        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        Service.perform_actions(args[0]) { |service_id|
            params = Hash.new
            params['owner_id'] = args[1]
            params['group_id'] = args[2] if args[2]

            json_action = Service.build_json_action('chown', params)

            client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action)
        }
    end

    chmod_desc = <<-EOT.unindent
        Changes the service permissions
    EOT

    command :chmod, chmod_desc, [:range, :service_id_list], :octet do
        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        Service.perform_actions(args[0]) { |service_id|
            params = Hash.new
            params['octet'] = args[1]

            json_action = Service.build_json_action('chmod', params)

            client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action)
        }
    end

    rename_desc = <<-EOT.unindent
        Renames the Service 
    EOT

    command :rename, rename_desc, :service_id, :name do
        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        params = Hash.new
        params['name'] = args[1]

        json_action = Service.build_json_action('rename', params)

        response = client.post("#{RESOURCE_PATH}/#{args[0]}/action", json_action)

        if CloudClient::is_error?(response)
            [response.code.to_i, response.to_s]
        else
            response.code.to_i
        end
    end

    action_desc = <<-EOT.unindent
        Perform an action on all the Virtual Machines of a given role.
        Actions supported: #{Role::SCHEDULE_ACTIONS.join(",")}
    EOT

    command :"action", action_desc, :service_id, :role_name, :vm_action,
        :options => [Service::PERIOD, Service::NUMBER] do

        client = Service::Client.new(
                    :username   => options[:username],
                    :password   => options[:password],
                    :url        => options[:server],
                    :user_agent => USER_AGENT)

        Service.perform_actions([args[0]]) { |service_id|
            params = Hash.new
            params[:period] = options[:period].to_i if options[:period]
            params[:number] = options[:number].to_i if options[:number]

            json_action = Service.build_json_action(args[2], params)

            client.post("#{RESOURCE_PATH}/#{service_id}/role/#{args[1]}/action", json_action)
        }
    end
end
